package glapp;

import java.nio.*;

import org.lwjgl.opengl.*;

/**
 *  Wrap the functionality of a FrameBufferObject.  A GLFrameBuffer is a virtual
 *  screen that can be rendered to just like the real screen, but can be
 *  any dimensions.  Rendered results are stored in a texture.
 *
 *  By default this class creates a FBO with a renderBuffer attached to provide
 *  depth test, and one texture to hold the rendered image.  Can attach more
 *  textures and pick which will be rendered to.
 *
 *  Example:
 *		GLFrameBuffer FBO;
 *
 *		init() {
 *			FBO = new GLFrameBuffer();    // by default setup a depth buffer and texture same size as display
 *		}
 *
 *		draw() {
 *			FBO.activate();
 *			// further rendering goes into FBO texture...
 *
 *			FBO.deactivate();
 *			// further rendering goes to screen
 *
 *			GLApp.activateTexture(FBO.getTexture(0));
 *			//... do more rendering with FBO texture...
 *		}
 *
 */
public class GLFrameBuffer {
	int FBOhandle = 0;     // FrameBuffer Object
	int RBhandle = 0;      // RenderBuffer Object (provides depth buffer)
	int Thandle = 0;       // Texture that we'll render into

	int width;				// dimensions of framebuffer
	int height;

	int[] renderTargets = new int[4];        // handles to additional textures that are attached
	int[] renderTargetTypes = new int[] {
			EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT,
			EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT,
			EXTFramebufferObject.GL_COLOR_ATTACHMENT2_EXT,
			EXTFramebufferObject.GL_COLOR_ATTACHMENT3_EXT
		};
	int targetCount = 0;

	int status = 0;  // status returned by OpenGL after FBO init
				// if this isn't == GL_FRAMEBUFFER_COMPLETE_EXT then init() had a problem

	int[] attachmentPoint = new int[] {
			EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, 
			EXTFramebufferObject.GL_COLOR_ATTACHMENT1_EXT,
			EXTFramebufferObject.GL_COLOR_ATTACHMENT2_EXT,
			EXTFramebufferObject.GL_COLOR_ATTACHMENT3_EXT 
			};

	/**
	 *  Create a framebuffer the same dimensions as current screen
	 */
	public GLFrameBuffer() {
		width = Display.getDisplayMode().getWidth();
		height = Display.getDisplayMode().getHeight();
		init();
	}

	/**
	 *  Create a framebuffer with the given dimensions
	 */
	public GLFrameBuffer(int w, int h) {
		// use given width, height
		width = w;
		height = h;
		init();
	}

	/**
	 *  Prepare framebuffer for use.  Width and height should be set to rational values
	 *  before calling this function.  Creates framebuffer with depth buffer and one texture.
	 */
	public int init() {
		// create a FBO handle
		FBOhandle = allocateFBO();

		// Bind this FBO to our current OpenGL context, so we can perform operations on it
		EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, FBOhandle);

		// create renderbuffer
		RBhandle = makeRenderBuffer(width,height);

		// attach to framebuffer
		addRenderBuffer(RBhandle);

		// make texture the same size as framebuffer
		Thandle = makeTexture();

		// attach texture to framebuffer
		addTexture(Thandle);

		// !!! add a second texture
		Thandle = makeTexture();
		addTexture(Thandle);

		// check for error condition
		status = checkStatus();

		// switch back to window-system-provided framebuffer
		EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);

		return status;
	}

	/**
	 *  return a new framebufferObject handle
	 */
    public static int allocateFBO()
    {
        IntBuffer handle = GLApp.allocInts(1);
		EXTFramebufferObject.glGenFramebuffersEXT(handle);
        return handle.get(0);
    }

	/**
	 *  Return a handle to a new texture the same size as the framebuffer.
	 */
	public int makeTexture() {
		//int thandle = GLApp.makeTexture(width,height);  // hack!!!
		int thandle = makeTexture(width,height);
		return thandle;
	}

    //================================================================================
    // HACK!!!  code from new GLApp
	
    /**
     * Create a blank texture with the given width and height.
     * @return  the texture handle
     */
    public static int makeTexture(int w, int h)
    {
        ByteBuffer pixels = GLApp.allocBytes(w*h*GLApp.SIZE_INT);  // allocate 4 bytes per pixel
        return GLApp.makeTexture(pixels, w, h, false);
	}

    //================================================================================
    
    /**
	 *  Attach a texture to framebuffer.  Texture must be same dimensions as framebuffer.
	 *  To render to different textures, create one FrameBufferObject and attach/detach
	 *  the textures to render to each.  It's faster to swap textures than to bind/unbind
	 *  FrameBuffers.
	 *
	 *  @see makeTexture
	 */
	public int addTexture(int thandle) {		
		// attach texture to the FBO so we can render to it
		// for now only attaches one texture to first attachment point
		EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, attachmentPoint[targetCount], GL11.GL_TEXTURE_2D, thandle, 0);

		// generate mipmaps for texture
		EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_2D);

		System.out.println("FBO add thandle=" + thandle + " at rendertarget=" +targetCount );

		// update render targets array
		renderTargets[targetCount] = thandle;
		targetCount++;

		//GLApp.msg("FBO add thandle=" + thandle);
		return thandle;
	}


	/**
	 *  Remove a texture from the framebuffer.  Texture still exists, but will no
	 *  longer be rendered to.
	 */
	public void removeTexture() {
		// attach texture 0 to the FBO
		// for now assumes one texture only on the first attachment point
		EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, 0, 0);
	}


	// should this take the attachment point as an arg (assuming depth buffer here)
	// renderbuffer was made with only gl_depth_component storage, so has to attach to depth_attachment.
	// if renderbuffer had RGB, BGRA, RGBA storage then it should be attached to a color_attachment point.
	public void addRenderBuffer(int rbhandle) {
		// attach renderbuffer to the FBO
		EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, rbhandle);
	}


	/**
	 *  Return the handle to a new renderBuffer, configured as a depth buffer
	 */
	 // assuming this is for depth buffer only (can render buffer also have color info in it?)
	public int makeRenderBuffer(int width, int height) {
		// get a handle to a renderbuffer:
		int bufferHandle = 0;
		IntBuffer depthbuffer = GLApp.allocInts(1);
		EXTFramebufferObject.glGenRenderbuffersEXT(depthbuffer);
		bufferHandle = depthbuffer.get(0);

		// bind the renderbuffer so that we can operate on it.
		EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, bufferHandle);

		// allocate storage space for the depth buffer
		// allocating only depth storage here (so should attach this rb to the depth attach point only)
		EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height);

		return bufferHandle;
	}


	/**
	 *  Return the error code from the FBO
	 */
	public int checkStatus() {
		// return the status of the currently bound FBO
		int status = EXTFramebufferObject.glCheckFramebufferStatusEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT);
		if (status != EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT) {
			//GLApp.err("FrameBufferObject.checkStatus(): framebuffer init failed, status is " + status);
			System.out.println("FrameBufferObject.checkStatus(): framebuffer init failed, status is " + status);
		}
		return status; 
	}


	/**
	 *  Once activated() all further rendering will go to the framebufferobject
	 *  @see deactivate()
	 */
	public void activate() {
		// Select the framebuffer for subsequent rendering operations
		EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, FBOhandle);

		// do I need to include the renderBuffer as a draw target?
		//glDrawBuffer(1, new int[] {GL_COLOR_ATTACHMENT0_EXT});
		GL11.glDrawBuffer(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT);

		// Set viewport to match fbo dimensions
		GL11.glPushAttrib(GL11.GL_VIEWPORT_BIT);
		GL11.glViewport(0, 0, width, height);
	}


	/**
	 *  Once deactivated all further rendering goes to the screen
	 */
	public void deactivate() {
		// return viewport to previous state
		GL11.glPopAttrib();

		// Stop rendering to FBO
		EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
	}


	// return a bytebuffer containing pixels from renderbuffer
	// does this make sense?  does a renderbuffer have color pixel values?
	public ByteBuffer getFramePixels(ByteBuffer pixels) {
		// fill pixels with renderbuffer pixels
		return null;
	}

	public ByteBuffer getFramePixels() {
		//return ByteBuffer pixels;
		return null;
	}


	/**
	 *  return handle of texture attached to framebuffer
	 * textures are numbered 0-N
	 */
	public int getTexture(int targetIndex) {
		if (targetIndex >= 0 && targetIndex < renderTargets.length) {
			return renderTargets[targetIndex];
		}
		return 0;
	}


	/**
	 *  delete the framebufferobject and renderbufferobject
	 */
	public void cleanup() {
		// FBO
		deleteFBO(FBOhandle);

		// render buffer
		deleteRenderBuffer(RBhandle);
	}


    /**
     * De-allocate the given FrameBufferObject.
     */
    public static void deleteFBO(int handle)
    {
        IntBuffer handle_buff = GLApp.allocInts(1).put(handle);
		EXTFramebufferObject.glDeleteFramebuffersEXT(handle_buff);
    }


    /**
     * De-allocate the given RenderBuffer.
     */
    public static void deleteRenderBuffer(int handle)
    {
        IntBuffer handle_buff = GLApp.allocInts(1).put(handle);
		EXTFramebufferObject.glDeleteRenderbuffersEXT(handle_buff);
    }


    public int getWidth() {
		return width;
	}


	public int getHeight() {
		return height;
	}


	public void drawTo(int targetNumber) {
		// need an array of all targets, created by addTexture()
		// this function creates an array of 1-4 renderTargets (ints)
		// renderTargets = new int[1];
		// activate() will call glDrawBuffer() with the renderTargets array
		// or maybe do it here once, leave it out of activate()

		GL11.glDrawBuffer(attachmentPoint[targetNumber]);
	}


	public void drawTo(int tn1, int tn2) {
		// renderTargets = new int[2];
	}


	public void drawTo(int tn1, int tn2, int tn3) {
		// renderTargets = new int[3];
	}


	public void drawTo(int[] targets) {
		// renderTargets = targets;
	}

}


